package com.whilelife.EDTInspection;

import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.daemon.GroupNames;
import com.intellij.codeInspection.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.ui.DocumentAdapter;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import java.awt.*;
import java.util.ArrayList;

/**
 * Created by IntelliJ IDEA.
 * Date: 2/27/12
 * Time: 9:25 PM
 * @author Brent Merle
 */
public class EDTInspection extends BaseJavaLocalInspectionTool {
  private static final com.intellij.openapi.diagnostic.Logger LOG = Logger.getInstance("#" + EDTInspection.class.toString());

  @NonNls
  public String ANNOTATION_FQN = "com.whilelife.annotation.EDTOnly";

  @Nls
  @NotNull
  @Override
  public String getGroupDisplayName() {
    return GroupNames.BUGS_GROUP_NAME;
  }

  @Nls
  @NotNull
  @Override
  public String getDisplayName() {
    return "@EDTOnly method doesn't check for EDT access";
  }

  @NotNull
  @Override
  public String getShortName() {
    return "EDTOnlyinspection";
  }



  @NotNull
  @Override
  public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
    return new JavaElementVisitor() {

      @Override
      public void visitReferenceExpression(PsiReferenceExpression psiReferenceExpression) {
      }

      @Override
      public void visitMethod(PsiMethod method) {
        ArrayList<String> annos = new ArrayList<String>();
        boolean isPrivateOrProtected = false;
        annos.add(ANNOTATION_FQN);

        PsiModifierList pml = method.getModifierList();
        for (PsiElement element : pml.getChildren()) {
          if (element.getText() == null) {
            continue;
          }
          if (element.getText().equals("private") || element.getText().equals("protected")) {
            isPrivateOrProtected = true;
          }
        }
        if (AnnotationUtil.isAnnotated(method, annos)) {
          PsiCodeBlock pcb = method.getBody();
          if (pcb != null) {
            if (isPrivateOrProtected) {
              checkPrivateMethod(pcb, method);
            } else {
              checkPublicMethod(pcb, method);
            }
          }
        }
      }
      private void checkPrivateMethod(@NotNull PsiCodeBlock pcb, @NotNull PsiMethod method ) {
        boolean hasAssert = false;
        boolean hasIsEDTCheck = false;
        for (PsiStatement statement : pcb.getStatements()) {
          LOG.info(statement.getText());
          if (hasAssert && hasIsEDTCheck) {
            LOG.info("found assert isEDT");
            break;
          }
          hasAssert = false;
          hasIsEDTCheck = false;
          for (PsiElement element : statement.getChildren()) {
            if (element.getText() == null) {
              continue;
            }
            String elementText = element.getText().toLowerCase().trim();
            if (elementText.equals("assert")) {
              LOG.info("found assert");
              hasAssert = true;
            } else if (hasAssert) {
              LOG.info("have assert, checking: " + elementText);
              if (elementText.matches("^\\(*swingutilities.iseventdispatchthread\\(\\)\\)*;*")) {
                LOG.info("got em both!! " + elementText);
                hasIsEDTCheck = true;
                break;
              }
            }
          }
        }
        if (!(hasAssert && hasIsEDTCheck)) {
          holder.registerProblem(method,"Privately " + ANNOTATION_FQN + " annotated method does not appear to assert SwingUtilities.isEventDispatchThread()", new MyQuickFix(true));
        } else {
          LOG.info("nope");
        }
      }

      private void checkPublicMethod(@NotNull PsiCodeBlock pcb, @NotNull PsiMethod method ) {
        boolean hasIf = false;
        boolean hasIsEDTCheck = false;
        for (PsiStatement statement : pcb.getStatements()) {
          LOG.info(statement.getText());
          if (hasIf && hasIsEDTCheck) {
            LOG.info("found if isEDT");
            break;
          }
          hasIf = false;
          hasIsEDTCheck = false;
          for (PsiElement element : statement.getChildren()) {
            if (element.getText() == null) {
              continue;
            }
            String elementText = element.getText().toLowerCase().trim();
            if (elementText.equals("if")) {
              LOG.info("found if");
              hasIf = true;
            } else if (hasIf) {
              LOG.info("have if, checking: " + elementText);
              if (elementText.matches("^\\(*!swingutilities.iseventdispatchthread\\(\\)\\)*\\{*")) {
                LOG.info("got em both!! " + elementText);
                hasIsEDTCheck = true;
                break;
              }
            }
          }
        }
        if (!(hasIf && hasIsEDTCheck)) {
          holder.registerProblem(method,"Publicly " + ANNOTATION_FQN + " annotated method does not appear to check if SwingUtilities.isEventDispatchThread()", new MyQuickFix(false));
        } else {
          LOG.info("nope");
        }
      }

    };
  }

  private class MyQuickFix implements LocalQuickFix {
    
    private final boolean m_isPrivateOrProtected;
    
    public MyQuickFix(boolean isPrivateOrProtected) {
      m_isPrivateOrProtected = isPrivateOrProtected;
    }

    @NotNull
    public String getName() {
      if (m_isPrivateOrProtected) {
        return "Add 'assert SwingUtilities.isEventDispatchThread();'";
      } 
        return "Add test for Event Dispatch Thread";
    }

    @NotNull
    public String getFamilyName() {
      return getName();
    }

    public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor problemDescriptor) {
      PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory();
      PsiMethod method = (PsiMethod)problemDescriptor.getPsiElement();
      PsiElement element;
      if (m_isPrivateOrProtected) {
        
        element = factory.createStatementFromText("assert SwingUtilities.isEventDispatchThread();", null);
        
      } else {
        element = factory.createStatementFromText("if (!SwingUtilities.isEventDispatchThread()) {\nthrow new RuntimeException(\"" + method.getName() + " must be called on the Event Dispatch Thread\");\n}", null);
      }

        PsiCodeBlock oldbody = method.getBody();
        if (oldbody != null){
          oldbody.addAfter(element, null);
        }


    }
  }
  
  
  @Override
  public JComponent createOptionsPanel() {
    JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
    final JTextField jtfAnnotationName = new JTextField(ANNOTATION_FQN);
    jtfAnnotationName.getDocument().addDocumentListener(new DocumentAdapter() {
      public void textChanged(DocumentEvent event) {
        ANNOTATION_FQN = jtfAnnotationName.getText();
      }
    });
    

    panel.add(jtfAnnotationName);
    return panel;
  }
}
